這些只是目前測試用的簡單版本,後續會考慮到提示次數、解題時間等,都會列入計分。
保留前兩天backend/dto.py的內容,加入以下程式碼
# backend/dto.py
from typing import List
class AssessmentProblem(BaseModel):
id: int
slug: str
title: str
difficulty: str
topic: str
model_config = ConfigDict(from_attributes=True)
class AssessmentStartOut(BaseModel):
user_id: int
problems: List[AssessmentProblem]
class AssessmentItemIn(BaseModel):
problem_id: int
verdict: Literal["accepted", "wrong", "tle", "skipped"] = "skipped"
hint_used: bool = False
class AssessmentScoreIn(BaseModel):
user_id: int
items: List[AssessmentItemIn]
class AssessmentResultOut(BaseModel):
user_id: int
level: Literal["beginner", "intermediate", "advanced"]
score: float
breakdown: dict
# backend/routers/assessment.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func
from ..database import SessionLocal
from .. import models
from ..dto import (
AssessmentStartOut, AssessmentProblem,
AssessmentScoreIn, AssessmentResultOut
)
router = APIRouter(prefix="/assessment", tags=["assessment"])
def get_db():
db = SessionLocal()
try: yield db
finally: db.close()
def pick_one(db: Session, difficulty: str):
# SQLite 的隨機排序
return (db.query(models.Problem)
.filter(models.Problem.is_active == True,
models.Problem.difficulty == difficulty)
.order_by(func.random()).first())
@router.get("/start", response_model=AssessmentStartOut)
def start_assessment(user_id: int, db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.id == user_id,
models.User.is_active == True).first()
if not user:
raise HTTPException(404, "user not found")
e = pick_one(db, "Easy")
m = pick_one(db, "Medium")
h = pick_one(db, "Hard")
problems = [p for p in [e, m, h] if p is not None]
if not problems:
raise HTTPException(400, "no problems available for assessment")
out = AssessmentStartOut(
user_id=user.id,
problems=[AssessmentProblem.model_validate(p) for p in problems]
)
return out
@router.post("/score", response_model=AssessmentResultOut)
def score_assessment(payload: AssessmentScoreIn, db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.id == payload.user_id).first()
if not user:
raise HTTPException(404, "user not found")
# 權重設定
diff_w = {"Easy": 1.0, "Medium": 2.0, "Hard": 3.0}
verdict_w = {"accepted": 1.0, "wrong": 0.2, "tle": 0.5, "skipped": 0.0}
total = 0.0
breakdown = {}
for it in payload.items:
p = db.query(models.Problem).get(it.problem_id)
if not p:
continue
base = diff_w.get(p.difficulty, 1.0) * verdict_w.get(it.verdict, 0.0)
if it.hint_used:
base = max(0.0, base - 0.2)
total += base
breakdown[str(p.id)] = {
"title": p.title,
"difficulty": p.difficulty,
"verdict": it.verdict,
"hint_used": it.hint_used,
"score": round(base, 2),
}
# 0~6 區間(最多 1+2+3=6)
score = round(total, 2)
if score > 4.0:
level = "advanced"
elif score >= 2.0:
level = "intermediate"
else:
level = "beginner"
# 寫回使用者等級
user.level = level
db.add(user); db.commit()
return AssessmentResultOut(
user_id=user.id,
level=level,
score=score,
breakdown=breakdown
)
把 assessment 路由註冊進 FastAPI,讓 assessment 可被存取。
在 app.py 裡 import 並 include
from .routers import assessment
...(原本的程式)
app.include_router(assessment.router)
做完以上步驟,就可以重啟後端:
python -m uvicorn backend.app:app --reload
Swagger 檢查:
GET /assessment/start?user_id=1 → 應回三題
POST /assessment/score:
{
"user_id": 1, //編號記得改
"items": [
{"problem_id": 1, "verdict": "accepted", "hint_used": false},
{"problem_id": 2, "verdict": "wrong", "hint_used": true},
{"problem_id": 3, "verdict": "skipped", "hint_used": false}
]
}
→ 應回 level, score, breakdown 並更新 users.level
在現有的前端後面,加入一個三題快速分級的功能:
抽題 → 顯示題目 → 在畫面選擇 verdict/hint_used → 一鍵送出 → 顯示等級與每題計分。
將程式接續到frontend/app.py後面:
# frontend/app.py
st.header("程度測驗(3 題快速分級)")
if "assessment_user_id" not in st.session_state:
st.session_state.assessment_user_id = 1 # 先固定 1,之後做登入再帶入
if "assessment_problems" not in st.session_state:
st.session_state.assessment_problems = None
if "assessment_results" not in st.session_state:
st.session_state.assessment_results = {}
col_a, col_b = st.columns([1, 3])
with col_a:
if st.button("開始測驗 / 重新抽題"):
try:
resp = requests.get(
f"{API_BASE}/assessment/start",
params={"user_id": st.session_state.assessment_user_id},
timeout=8
)
st.session_state.assessment_problems = resp.json()["problems"]
st.session_state.assessment_results = {} # reset
st.success("已抽出 3 題,請回報結果")
except Exception as e:
st.error(f"抽題失敗:{e}")
if st.session_state.assessment_problems:
st.write("請到 LeetCode 作答,或根據理解回報本題結果:")
for p in st.session_state.assessment_problems:
st.markdown(
f"- **[{p['difficulty']}] {p['title']}** / topic: {p['topic']} / "
f"slug: `{p['slug']}`"
)
verdict = st.selectbox(
f"結果(題目 #{p['id']})",
["accepted", "wrong", "tle", "skipped"],
key=f"verdict_{p['id']}"
)
hint_used = st.checkbox(
f"該題有使用提示(題目 #{p['id']})",
key=f"hint_{p['id']}"
)
st.session_state.assessment_results[p["id"]] = {
"problem_id": p["id"], "verdict": verdict, "hint_used": hint_used
}
if st.button("送出並計分"):
try:
payload = {
"user_id": st.session_state.assessment_user_id,
"items": list(st.session_state.assessment_results.values())
}
resp = requests.post(f"{API_BASE}/assessment/score", json=payload, timeout=10)
data = resp.json()
st.success(f"建議等級:**{data['level']}**(score={data['score']})")
with st.expander("查看每題計分"):
for pid, info in data["breakdown"].items():
st.write(f"- #{pid} {info['title']} / {info['difficulty']} → "
f"{info['verdict']} / hint={info['hint_used']} / score={info['score']}")
except Exception as e:
st.error(f"計分失敗:{e}")
隨機抽出三題,一題Easy,一題Medinum,一題Hard:
做完後可選擇解題狀態(Accepted、WA、TLE...)
送出後可查看等級與每題計分。